
package gameserver;

import common.Client;
import common.Latency;
import common.ReceiveHandleLoop;
import common.State;
import common.TimeoutHandler;
import common.packet.Authorization;
import common.packet.ChatMessage;
import common.packet.ClientInfo;
import common.packet.Input;
import common.packet.Packet;
import common.packet.PacketHandler;
import common.packet.Error;
import common.packet.OK;
import common.PingHandler;
import java.io.IOException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * Stores data for a connected client and provides methods to control it. It owns
 * the RHL of the client and runs it in its own run method. It performs the
 * authorization process when it is first started and adds and removes packet
 * handlers to its RHL as necessary. All its packet handlers are inner classes
 * because they don't make sense outside of here.
 *
 * @author xissburg
 */
public class ClientThread implements Runnable
{
    private final Game game;
    private final ReceiveHandleLoop receiveHandleLoop;
    private Latency latency;

    /**
     * Stores the last input packets sent by the client. The InputHandler below
     * adds stuff into this list.
     */
    private final List<Input> inputList;

    public ClientThread(Client client, final Game game)
    {
        this.game = game;
        latency = new Latency(client);
        inputList = new ArrayList<Input>();
        receiveHandleLoop = new ReceiveHandleLoop(client);

        //setup the error handler for the authorization process
        receiveHandleLoop.setErrorHandler(new ReceiveHandleLoop.ErrorHandler()
        {
            public boolean onSocketTimeoutException(SocketTimeoutException ex)
            {
                try { //Try to send error message
                    receiveHandleLoop.getClient().write(new Error("Timeout exception."));
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                }
                return false;
            }

            public boolean onIOException(IOException ex) {
                ex.printStackTrace();
                return false;
            }
        });
    }

    public void addPacketHandler(PacketHandler handler) {
        receiveHandleLoop.addPacketHandler(handler);
    }

    public void removePacketHandler(Packet.Code code) {
        receiveHandleLoop.removePacketHandler(code);
    }

    public Client getClient() {
        return receiveHandleLoop.getClient();
    }

    /**
     * Returns a list of the last input packets received from the client.
     * @return
     */
    public List<Input> getInput()
    {
        synchronized(inputList)
        {
            if(inputList.isEmpty())
                return null;

            List<Input> input = new ArrayList<Input>(inputList);
            inputList.clear();

            return input;
        }
    }

    public long getLatency() {
        return latency.getLatency();
    }

    public boolean refreshLatency() {
        return latency.refreshLatency();
    }

    public void run()
    {
        Client client = receiveHandleLoop.getClient();
        //Set 8 seconds timeout
        try {
            client.setSocketTimeout(8000);
        }
        catch (SocketException ex) {
            try {
                client.closeSocket();
            } catch (IOException ex1) {
                ex1.printStackTrace();
            } finally {
                return;
            }
        }

        //Add one AuthorizationHandler
        addPacketHandler(new AuthorizationHandler());

        //Set the default packet handler for this operation
        receiveHandleLoop.setDefaultHandler(new PacketHandler() {
            public void handle(Packet packet) {
                try { //Try to send error message
                    Client client = receiveHandleLoop.getClient();
                    client.write(new Error("Invalid object. Authorization packet required."));
                    receiveHandleLoop.stop();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }

            public Packet.Code getPacketCode() {
                return null;
            }
        });

        //And go
        receiveHandleLoop.run();
    }

    
    //---------------------------------------------------------------------------------
    /**
     * Packet Handlers for this class. They are very specific for this class
     * only, thats why they are private inner classes. They don't make sense
     * outside of here.
     */

    // <editor-fold defaultstate="collapsed" desc="local packet handlers">

    /**
     * Handles the Authorization packet sent during the authorization process.
     * It setsup the initial client structure with data from the Authorization
     * packet, removes itself from the RHL, adds an OKHandler to it, sets a new
     * default handler just t send a different error message to the client and
     * send an OK packet to the client to notify he was successfully authorized
     * and must now send another OK packet to notify the server he is ready to
     * continue.
     */
    private class AuthorizationHandler implements PacketHandler
    {
        public AuthorizationHandler() {}

        public void handle(Packet packet) {
            Authorization auth = (Authorization) packet;
            String name = auth.getUsername();
            Client client = receiveHandleLoop.getClient();

            if(name.length() < 3)
            {
                try { //Try to send error message
                    client.write(new Error("User name must be at least 3 characters long."));
                    receiveHandleLoop.stop();
                } catch (IOException ex) {
                    ex.printStackTrace();
                } finally {
                    return;
                }
            }

            //Check whether the name is unique among all existing clients
            Collection<ClientThread> clientCol = game.getClients();
            for (ClientThread ct : clientCol) {
                if(name.equals(ct.getClient().getClientInfo().getName()))
                {
                    try {
                        client.write(new Error("User name currently in use, please try another one."));
                        receiveHandleLoop.stop();
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    } finally {
                        return;
                    }
                }
            }
            
            client.getClientInfo().setName(name);

            //Authentication successful. Remove this handler and add an OK handler,
            //because this is what the server hopes ro receive from the client.
            receiveHandleLoop.removePacketHandler(getPacketCode());
            receiveHandleLoop.addPacketHandler(new OKHandler());

            //Set the default packet handler for this operation
            receiveHandleLoop.setDefaultHandler(new PacketHandler() {

                public void handle(Packet packet) {
                    try { //Try to send error message
                        Client client = receiveHandleLoop.getClient();
                        client.write(new Error("Invalid object. OK packet required."));
                        receiveHandleLoop.stop();
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }

                public Packet.Code getPacketCode() {
                    return null;
                }
            });

            //Send OK packet
            try {
                client.write(new OK());
            } catch (IOException ex) {
                try { //If it fails stop the recv-handle loop
                    client.closeSocket();
                    receiveHandleLoop.stop();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                }
            }
        }

        public Packet.Code getPacketCode() {
            return Packet.Code.AUTHORIZATION;
        }
    }

    /**
     * Handles an OK packet in the authorization process. Now the client is ready
     * to continue. First it resets the client's socket timeout to infinity,
     * then removes itself from the RHL, sets the default handler to none, adds
     * this client thread to the game, sends the current game state to the client,
     * and adds the another handlers to RHL: ChatMessage, ClientState and Logout,
     * and also if the client is the owner/first ti also adds a StartGameHandler.
     *
     */
    private class OKHandler implements PacketHandler
    {
        public OKHandler() {}

        public void handle(Packet packet) {
            Client client = receiveHandleLoop.getClient();
            //Now the client was accepted and it is ready to continue.
            //First set its socket timeout to the maximum innactivity time
            int maxInactivityTime = 10000;
            try {
                client.setSocketTimeout(maxInactivityTime);
            } catch (SocketException ex) {
                try {
                    client.closeSocket();
                    receiveHandleLoop.stop();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    return;
                }
            }

            //Remove this OK handler, set the default to null and set the error
            //handler to be a TimeoutHandler
            receiveHandleLoop.removePacketHandler(getPacketCode());
            receiveHandleLoop.setDefaultHandler(null);
            receiveHandleLoop.setErrorHandler(
                    new TimeoutHandler(receiveHandleLoop, 8000));

            //Set the error handler to be a
            //receiveHandleLoop.setErrorHandler(new )

            //Setup initial client info
            //...

            //Set the finish code. If the receive-handle loop terminates, it means
            //the client is dead. Then it must notify all other clients and remove
            //this dead client from the global client list.
            receiveHandleLoop.setFinishRunnable(new Runnable() {
                public void run() {
                    Client thisClient = receiveHandleLoop.getClient();

                    //Remove client from the global client list
                    game.removeClientThread(thisClient.getClientInfo().getName());

                    try { //make sure the socket is closed
                        thisClient.closeSocket();
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }

                    System.out.println(thisClient.getClientInfo().getName() + " left the game.");
                }
            });

            //Now the client is ready to be added to the global client list. Now he
            //may receive packets written from other ClientThreads. Also, notify all
            //other clients about this new client, if any.
            game.addClientThread(ClientThread.this);

            try { //Send the current game state
                client.write(game.getGameState());
            } catch (IOException ex) {
                try {
                    receiveHandleLoop.stop();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                } finally {
                    return;
                }
            }

            //Now setup the actual handlers for the client
            receiveHandleLoop.addPacketHandler(new PingHandler(client));
            receiveHandleLoop.addPacketHandler(latency.getPongHandler());
            receiveHandleLoop.addPacketHandler(new ChatMessageHandler());
            receiveHandleLoop.addPacketHandler(new LogoutHandler());
            receiveHandleLoop.addPacketHandler(new TimeoutHandler.
                    AreYouAliveHandler(receiveHandleLoop.getClient()));

            if(game.getState() == State.WAITING) {
                receiveHandleLoop.addPacketHandler(new ClientInfoHandler());
            }

            //only the onwer can start the game, hence add a StartGameHandler for it only
            if (client.getClientInfo().isOwner())
                receiveHandleLoop.addPacketHandler(new StartGameHandler());

            System.out.println(client.getClientInfo().getName() + " joined the game.");
        }

        public Packet.Code getPacketCode() {
            return Packet.Code.OK;
        }
    }

    /**
     * Stops a ReceiveHandleLoop consequently removing all resources allocated to this
     * client in the game server and also notifies all other clients (the finishRunnable
     * of the receiveHandleLoop does the job).
     *
     * @author xissburg
     */
    private class LogoutHandler implements PacketHandler
    {
        public void handle(Packet packet)
        {
            try {
                receiveHandleLoop.stop();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }

        public Packet.Code getPacketCode() {
            return Packet.Code.LOGOUT;
        }
    }

    /**
     * Handles a client state change request sent by the client to the server.
     *
     * @author xissburg
     */
    class ClientInfoHandler implements PacketHandler
    {
        public void handle(Packet packet)
        {
            ClientInfo clientInfo = (ClientInfo)packet;
            game.setClientInfo(clientInfo);
        }

        public Packet.Code getPacketCode() {
            return Packet.Code.CLIENT_INFO;
        }
    }

    /**
     * Handles a chat message received by the game server.
     *
     * @author xissburg
     */
    public class ChatMessageHandler implements PacketHandler
    {
        @Override
        public void handle(Packet packet)
        {
            ChatMessage chatMessage = (ChatMessage)packet;
            String message = chatMessage.getMessage();
            Client client = getClient();
            //Make sure the message is not empty or null
            if(message != null && message.length() > 0)
            {
                ChatMessage cmsg = new ChatMessage("[" +
                        client.getClientInfo().getName() + "] " + message);

                Collection<ClientThread> clients = game.getClients();

                for(ClientThread ct: clients)
                {
                    try {
                        ct.getClient().write(cmsg);
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }

        public Packet.Code getPacketCode()
        {
            return Packet.Code.CHAT_MESSAGE;
        }
    }

    private class StartGameHandler implements PacketHandler
    {
        public StartGameHandler() {}

        public void handle(Packet packet) {
            //Remove the StartGame handler from the RHL since the game cannot be started
            //while it is already running
            receiveHandleLoop.removePacketHandler(getPacketCode());

            Collection<ClientThread> clients = game.getClients();

            //Remove the ClientInfo handler from all clients
            for (ClientThread ct : clients)
                ct.removePacketHandler(Packet.Code.CLIENT_INFO);

            //Add Input handlers to all clients which are players
            for (ClientThread ct : clients)
            {
                if(ct.getClient().getClientInfo().getType() == ClientInfo.Type.PLAYER)
                {
                    InputHandler inputHandler = ct.new InputHandler();
                    ct.addPacketHandler(inputHandler);
                }
            }

            //Start game
            game.start();
        }

        public Packet.Code getPacketCode() {
            return Packet.Code.START_GAME;
        }
    }
    
    private class InputHandler implements PacketHandler
    {
        public InputHandler() {}
        
        public void handle(Packet packet)
        {
            Input input = (Input)packet;
            input.setTimestamp(System.nanoTime());

            synchronized(inputList) {
                inputList.add(input);
            }
        }

        public Packet.Code getPacketCode() {
            return Packet.Code.INPUT;
        }
    }// </editor-fold>
}
